/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.service.util;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.parsers.html.HTMLPlainTextExtractor;
import com.blandware.atleap.common.util.ConvertUtil;
import com.blandware.atleap.common.util.RegExUtil;
import com.blandware.atleap.common.util.StringUtil;
import com.blandware.atleap.model.core.ContentField;
import com.blandware.atleap.model.core.ContentFieldValue;
import com.blandware.atleap.model.core.ContentLocale;
import com.blandware.atleap.model.core.ContentResource;
import com.blandware.atleap.model.core.MailTemplate;
import com.blandware.atleap.persistence.core.ContentLocaleDAO;
import com.blandware.atleap.persistence.core.ContentResourceDAO;
import com.blandware.atleap.persistence.core.MailTemplateDAO;
import com.blandware.atleap.service.exception.BeanNotFoundException;
import com.blandware.atleap.service.exception.MailAddressException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.VelocityException;
import org.springframework.core.io.InputStreamSource;
import org.springframework.mail.MailPreparationException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.ui.velocity.VelocityEngineUtils;

import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;


/**
 * <p>Mail engine used to send e-mails via Spring Mail abstraction layer
 * </p>
 * <p><a href="MailEngine.java.html"><i>View Source</i></a>
 * </p>
 *
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.17 $ $Date: 2006/08/03 10:07:45 $
 */
public class MailEngine {

	/**
	 * Log instance for our class
	 */
	protected transient final Log log = LogFactory.getLog(MailEngine.class);

	/**
	 * Mail sender to use
	 */
	protected JavaMailSender mailSender;

	/**
	 * Velocity engine to merge templates into string
	 */
	protected VelocityEngine velocityEngine;

	/**
	 * DAO for work with mail templates
	 */
	protected MailTemplateDAO mailTemplateDAO;

	/**
	 * DAO for work with content locales
	 */
	protected ContentLocaleDAO contentLocaleDAO;

	/**
	 * DAO for work with content resources
	 */
	protected ContentResourceDAO contentResourceDAO;

	/**
	 * Sets a mail sender to use
	 *
	 * @param mailSender the mail sender to set
	 */
	public void setMailSender(JavaMailSender mailSender) {
		this.mailSender = mailSender;
	}

	/**
	 * Sets the Velocity engine to use
	 *
	 * @param velocityEngine the Velocity engine to set
	 */
	public void setVelocityEngine(VelocityEngine velocityEngine) {
		this.velocityEngine = velocityEngine;
	}

	/**
	 * Sets the mail template DAO that will be used to work with the persistance
	 * layer
	 *
	 * @param mailTemplateDAO the mail template DAO to set
	 */
	public void setMailTemplateDAO(MailTemplateDAO mailTemplateDAO) {
		this.mailTemplateDAO = mailTemplateDAO;
	}

	/**
	 * Sets the content locale DAO that will be used to work with the persistance
	 * layer
	 *
	 * @param contentLocaleDAO the content locale DAO to set
	 */
	public void setContentLocaleDAO(ContentLocaleDAO contentLocaleDAO) {
		this.contentLocaleDAO = contentLocaleDAO;
	}

	/**
	 * Sets the content resource DAO that will be used to work with the persistance
	 * layer
	 *
	 * @param contentResourceDAO the content resource DAO to set
	 */
	public void setContentResourceDAO(ContentResourceDAO contentResourceDAO) {
		this.contentResourceDAO = contentResourceDAO;
	}

	/**
	 * Sends message to several recipients.
	 *
	 * @param from               Sender of the message
	 * @param subject            Subject of the message
	 * @param to                 Recipients of message
	 * @param cc                 Carbon copy - list of additional addresses to set
	 * @param bcc                Blind carbon copy - list of additional addresses, hidden from other recipients
	 * @param templateIdentifier Identifier of template to use
	 * @param locale             Language to send message on
	 * @param model              Mappings of variables used in template to their values
     * @param encodeModel        Whether to HTML-encode model
	 * @throws MailPreparationException if mail message could not be prepared properly
	 */
	public void sendMessage(String from, String subject, String[] to, String[] cc, String[] bcc, String templateIdentifier, String locale, Map model, boolean encodeModel) {

		if ( to == null || to.length == 0 ) {
			throw new MailPreparationException("No recipient specified");
		}

		MimeMessage message = null;
		try {
			message = createMessage(from, subject, to, cc, bcc, templateIdentifier, locale, model, encodeModel);
		} catch ( Exception e ) {
			throw new MailPreparationException(e);
		}
		send(message);
	}

	/**
	 * Sends message to several recipients. Model is encoded.
	 *
	 * @param from               Sender of the message
	 * @param subject            Subject of the message
	 * @param to                 Recipients of message
	 * @param cc                 Carbon copy - list of additional addresses to set
	 * @param bcc                Blind carbon copy - list of additional addresses, hidden from other recipients
	 * @param templateIdentifier Identifier of template to use
	 * @param locale             Language to send message on
	 * @param model              Mappings of variables used in template to their values
	 * @throws MailPreparationException if mail message could not be prepared properly
	 */
	public void sendMessage(String from, String subject, String[] to, String[] cc, String[] bcc, String templateIdentifier, String locale, Map model) {
        sendMessage(from, subject, to, cc, bcc, templateIdentifier, locale, model, true);
    }

	/**
	 * Sends message to several recipients. 'From' and 'subject' will be taken
	 * from the template.
	 *
	 * @param to                 Recipients of message
	 * @param cc                 Carbon copy - list of additional addresses to set
	 * @param bcc                Blind carbon copy - list of additional addresses, hided from other recipients
	 * @param templateIdentifier Identifier of template to use
	 * @param locale             Language to send message on
	 * @param model              Mappings of variables used in template to their values
	 * @throws MailPreparationException if mail message could not be prepared properly
	 */
	public void sendMessage(String[] to, String[] cc, String[] bcc, String templateIdentifier, String locale, Map model) {
		sendMessage(null, null, to, cc, bcc, templateIdentifier, locale, model);
	}

	/**
	 * Sends message to several recipients. 'From' and 'subject' will be taken
	 * from the template.
	 *
	 * @param to                 Recipients of message
	 * @param templateIdentifier Identifier of template to use
	 * @param locale             Language to send message on
	 * @param model              Mappings of variables used in template to their values
	 * @throws MailPreparationException if mail message could not be prepared properly
	 */
	public void sendMessage(String[] to, String templateIdentifier, String locale, Map model) {
		sendMessage(to, null, null, templateIdentifier, locale, model);
	}

	/**
	 * Sends message to recipient. 'From' and 'subject' will be taken
	 * from the template.
	 *
	 * @param to                 Recipient of message
	 * @param templateIdentifier Identifier of template to use
	 * @param locale             Language to send message on
	 * @param model              Mappings of variables used in template to their values
	 * @throws MailPreparationException if mail message could not be prepared properly
	 */
	public void sendMessage(String to, String templateIdentifier, String locale, Map model) {
		sendMessage(new String[]{to}, templateIdentifier, locale, model);
	}

    /**
     * Creates simple or MIME message according to template's MIME-type. Model
     * is encoded.
     *
     * @param from               Sender of the message
     * @param subject            Subject of the message
	 * @param to                 Recipients of message
	 * @param cc                 Carbon copy - list of additional addresses to set
	 * @param bcc                Blind carbon copy - list of additional addresses, hided from other recipients
	 * @param templateIdentifier Identifier of template to use
	 * @param locale             Language to send message on
	 * @param model              Mappings of variables used in template to their values
     * @return created message
     * @throws BeanNotFoundException
     * @throws VelocityException
     */
    protected MimeMessage createMessage(String from, String subject, String[] to, String[] cc, String[] bcc, String templateIdentifier, String locale, Map model) throws BeanNotFoundException, VelocityException {
        return createMessage(from, subject, to, cc, bcc, templateIdentifier, locale, model, true);
    }

	/**
	 * Creates simple or MIME message according to template's MIME-type
	 *
	 * @param from               Sender of the message
	 * @param subject            Subject of the message
	 * @param to                 Recipients of message
	 * @param cc                 Carbon copy - list of additional addresses to set
	 * @param bcc                Blind carbon copy - list of additional addresses, hided from other recipients
	 * @param templateIdentifier Identifier of template to use
	 * @param locale             Language to send message on
	 * @param model              Mappings of variables used in template to their values
     * @param encodeModel        Whether to HTML-encode model
     * @return created message
	 */
	protected MimeMessage createMessage(String from, String subject, String[] to, String[] cc, String[] bcc, String templateIdentifier, String locale, Map model, boolean encodeModel) throws BeanNotFoundException, VelocityException {
		MailTemplate mailTemplate = mailTemplateDAO.findMailTemplateByIdentifier(templateIdentifier);
		if ( mailTemplate == null ) {
			throw new BeanNotFoundException("No template with identifier '" + templateIdentifier + "' could be found");
		}
		MimeMessageHelper messageHelper = null;
		String charset = (String) mailTemplate.getCharset().get(locale);
		if ( charset == null || charset.length() == 0 ) {
			charset = Constants.DEFAULT_ENCODING;
		}

		boolean plain = mailTemplate.isPlain();

		if ( from == null || from.length() == 0 ) {
			from = mergeTemplate(templateIdentifier, "from", locale, model, encodeModel);
		}

		if ( subject == null || subject.length() == 0 ) {
			subject = mergeTemplate(templateIdentifier, "subject", locale, model, encodeModel);
		}

		String text = mergeTemplate(templateIdentifier, "body", locale, model, encodeModel);

		// replace encoded line breaks (&lt;br /&gt;) with normal tags (<br />);

        if (encodeModel) {
            try {
                text = RegExUtil.replaceAll(text, "&lt;br\040*/?&gt;", "<br />");
            } catch ( MalformedPatternException e ) {
                // ignore
            }
        }

		InternetAddress fromAddress = prepareAddress(from, charset);
		InternetAddress[] toAddresses = prepareAddresses(to, charset);

		try {
			messageHelper = createEmptyMessageHelper(charset, !plain, false);
			messageHelper.setFrom(fromAddress);
			messageHelper.setReplyTo(fromAddress);
			messageHelper.setSubject(subject);
			messageHelper.setTo(toAddresses);
			if ( cc != null ) {
				InternetAddress[] ccAddresses = prepareAddresses(cc, charset);
				messageHelper.setCc(ccAddresses);
			}
			if ( bcc != null ) {
				InternetAddress[] bccAddresses = prepareAddresses(bcc, charset);
				messageHelper.setBcc(bccAddresses);
			}
			messageHelper.setSentDate(new Date());
			if ( !plain ) {
				prepareMimeMessage(messageHelper, text);
			} else {
				// html flag set to tru, because all encoding of HTML-sensitive characters has been performed during
				// preparation of body text
				messageHelper.setText(text, true);
			}
		} catch ( MessagingException e ) {
			throw new MailPreparationException(e);
		}

		return messageHelper.getMimeMessage();
	}

	/**
	 * Creates simple plain message
	 *
	 * @param charset   Charset
	 * @param multipart Whether the message is multipart
	 * @param validate  Whether or not to validate
	 * @return Empty MIME message
	 */
	protected MimeMessageHelper createEmptyMessageHelper(String charset, boolean multipart, boolean validate) throws MessagingException {
		MimeMessage message = mailSender.createMimeMessage();
		MimeMessageHelper messageHelper = new MimeMessageHelper(message, multipart, charset);
		messageHelper.setValidateAddresses(validate);
		return messageHelper;
	}

	/**
	 * Adds inline resources and attachements to MIME message
	 *
	 * @param helper MIME message helper used to add inline resources and attachements
	 * @param text   Body text of the message
	 */
	protected void prepareMimeMessage(MimeMessageHelper helper, String text) {
		try {
			HTMLPlainTextExtractor extractor = new HTMLPlainTextExtractor();
			Set resourceUris = new HashSet(extractor.extractInlineResources(new ByteArrayInputStream(ConvertUtil.convertToByteArray(text)), Constants.DEFAULT_ENCODING));
			Map resourcesMap = new HashMap();
			for ( Iterator i = resourceUris.iterator(); i.hasNext(); ) {
				String src = (String) i.next();
				String prefix = Constants.RESOURCES_URI_PREFIX;
				if ( prefix.startsWith("/") ) {
					prefix = prefix.substring(1);
				}

				int k = src.indexOf(prefix);
				if ( k == -1 ) {
					// that's not a link to inline resource, so skip it
					continue;
				}

				String resourceUri = src.substring(k);
				if ( !resourceUri.startsWith("/") ) {
					resourceUri = "/" + resourceUri;
				}

				ContentResource resource = contentResourceDAO.findContentResourceByUri(resourceUri);
				if ( resource != null ) {
					String cid = "resource" + resource.getId();
					resourcesMap.put(cid, resource);
					text = text.replaceAll("=\"" + src + "\"", "=\"cid:" + cid + "\"");
				}
			}

			helper.setText(text, true);

			// add resources
			for ( Iterator i = resourcesMap.entrySet().iterator(); i.hasNext(); ) {
				Map.Entry entry = (Map.Entry) i.next();
				String cid = (String) entry.getKey();
				ContentResource resource = (ContentResource) entry.getValue();
				helper.addInline(cid, new InputStreamContentResource(resource), resource.getMimeType());
			}

		} catch ( Exception e ) {
			throw new MailPreparationException(e);
		}

	}

	/**
	 * Creates instance of <code>javax.mail.internet.InternetAddress</code> with correctly encoded personal
	 *
	 * @param address String representation of address
	 * @param charset Charset to use to encode personal
	 * @return Instance of <code>javax.mail.internet.InternetAddress</code> with correctly encoded personal
	 * @see javax.mail.internet.InternetAddress
	 */
	public InternetAddress prepareAddress(String address, String charset) {
		return prepareAddresses(new String[]{address}, charset)[0];
	}

	/**
	 * Creates array of instances of of <code>javax.mail.internet.InternetAddress</code> with correctly encoded personals. <br />
	 * Wrap all exceptions in our runtime equivalent.
	 *
	 * @param addresses Array of string representation of addresses
	 * @param charset   Charset to use to encode personal
	 * @return Array of instances of of <code>javax.mail.internet.InternetAddress</code> with correctly encoded personals
	 * @see javax.mail.internet.InternetAddress
	 * @see com.blandware.atleap.service.exception.MailAddressException
	 */
	public InternetAddress[] prepareAddresses(String[] addresses, String charset) {
		InternetAddress[] internetAddresses = new InternetAddress[addresses.length];
		try {
			for ( int i = 0; i < addresses.length; i++ ) {
				String address = addresses[i];
				InternetAddress internetAddress = new InternetAddress(address);
				internetAddress.setPersonal(internetAddress.getPersonal(), charset);
				internetAddresses[i] = internetAddress;
			}
		} catch ( Exception e ) {
			throw new MailAddressException(e.getLocalizedMessage());
		}
		return internetAddresses;
	}

	/**
	 * Sends specified message using mail sender
	 *
	 * @param msg Message (simple or MIME) to send
	 * @throws org.springframework.mail.MailException
	 *          if mail sending failed
	 */
	protected void send(MimeMessage msg) {
		mailSender.send(msg);
	}

	/**
	 * Merges template replacing HTML-sensitive characters with their entity equivalents
	 *
	 * @param identifier Identifier of mail template
	 * @param field      Name of field (from, subject or body)
	 * @param locale     Identifier of locale
	 * @param model      Mappings of variables used in template to their values
	 * @return Merged template
	 */
	public String mergeTemplate(String identifier, String field, String locale, Map model) throws VelocityException {
		return mergeTemplate(identifier, field, locale, model, true);
	}


	/**
	 * Merges template
	 *
	 * @param identifier  Identifier of mail template
	 * @param field       Name of field (from, subject or body)
	 * @param locale      Identifier of locale
	 * @param model       Mappings of variables used in template to their values
	 * @param encodeModel Whether or not to replace HTML-sensitive characters with their entity equivalents
	 * @return Merged template
	 */
	public String mergeTemplate(String identifier, String field, String locale, Map model, boolean encodeModel) throws VelocityException {
		String result = null;

		// retrieve template
		MailTemplate mailTemplate = mailTemplateDAO.findMailTemplateByIdentifier(identifier);
		if ( mailTemplate == null ) {
			throw new ResourceNotFoundException("Mail template with identifier '" + identifier + "' could not be found");
		}

		ContentField requestedField = (ContentField) mailTemplate.getContentFieldsMap().get(field);
		if ( requestedField == null ) {
			throw new ResourceNotFoundException("Content field with identifier '" + field + "' could not be found on mail template '" + identifier + "' (ID=" + mailTemplate.getId() + ")");
		}

		Map contentFieldValues = requestedField.getContentFieldValuesMap();
		ContentFieldValue templateValue = null;
		if ( contentFieldValues != null && !contentFieldValues.isEmpty() ) {
			// try to search by given locale
			templateValue = (ContentFieldValue) contentFieldValues.get(locale);

			// if not found, search by default locale
			if ( templateValue == null || isEmpty(templateValue) ) {

				if ( log.isWarnEnabled() ) {
					log.warn("No value of content field with ID=" + requestedField.getId() + " could be found for locale '" + locale + "' Trying to search by default locale...");
				}

				ContentLocale defaultLocale = contentLocaleDAO.findDefaultContentLocale();
				templateValue = (ContentFieldValue) contentFieldValues.get(defaultLocale.getIdentifier());

				// if still not found get first value in map
				if ( templateValue == null || isEmpty(templateValue) ) {
					if ( log.isWarnEnabled() ) {
						log.warn("No value for default locale could be found. Getting the first one...");
					}
					templateValue = (ContentFieldValue) contentFieldValues.values().iterator().next();
				}
			}
		} else {
			throw new ResourceNotFoundException("No content field value could be found on field with ID=" + requestedField.getId());
		}

		// first look for binary data
		String template = ConvertUtil.convertToString(templateValue.getValue());

		// if not found, look for simple value
		if ( template == null ) {
			template = templateValue.getSimpleValue();
		}

		template = "mailTemplate: " + template;

		if ( encodeModel ) {
			model = StringUtil.htmlEncodeModel(model);
		}
		result = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, template, model);

		return result;
	}

	/**
	 * Checks CFV for emptyness
	 *
	 * @param value Content field value to check for emptyness
	 * @return <code>true</code> if simple value is null or an empty string and value is null or an empty array. Otherwise, returns <code>false</code>
	 */
	protected boolean isEmpty(ContentFieldValue value) {
		return (value.getSimpleValue() == null || value.getSimpleValue().length() == 0) && (value.getValue() == null || value.getValue().length == 0);
	}

	/**
	 * Wrapper class for content resource which can return resource as input stream
	 */
	protected static class InputStreamContentResource implements InputStreamSource {

		/**
		 * Wrapped content resource
		 */
		protected ContentResource contentResource;

		/**
		 * Creates new instance and wraps specified content resource
		 *
		 * @param contentResource Content resource to wrap
		 */
		public InputStreamContentResource(ContentResource contentResource) {
			this.contentResource = contentResource;
		}

		/**
		 * @see org.springframework.core.io.InputStreamSource#getInputStream()
		 */
		public InputStream getInputStream() throws IOException {
			InputStream stream = null;
			if ( contentResource != null ) {
				stream = new ByteArrayInputStream(contentResource.getResourceData().getData());
			}
			return stream;
		}
	}
}
